/* @flow */

import {install} from './install'
import {START} from './util/route'
import {assert} from './util/warn'
import {inBrowser} from './util/dom'
import {cleanPath} from './util/path'
import {createMatcher} from './create-matcher'
import {normalizeLocation} from './util/location'
import {supportsPushState} from './util/push-state'

import {HashHistory} from './history/hash'
import {HTML5History} from './history/html5'
import {AbstractHistory} from './history/abstract'

import type {Matcher} from './create-matcher'

export default class VueRouter {
    static install: () => void;
    static version: string;

    //与当前VueRouter实例关联的第一个vue实例对象
    app: any;
    //与当前VueRouter实例关联的所有vue实例对象，一个VueRouter实例可以被注入多个
    //不同的vue实例中
    apps: Array<any>;
    ready: boolean;
    readyCbs: Array<Function>;
    options: RouterOptions;
    //当前history的模式，可能的值为'history|hash|abstract'
    mode: string;
    history: HashHistory | HTML5History | AbstractHistory;
    matcher: Matcher;
    //当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true
    fallback: boolean;
    //全局守卫
    beforeHooks: Array<?NavigationGuard>;
    //全局解析守卫
    resolveHooks: Array<?NavigationGuard>;
    //全局后置钩子
    afterHooks: Array<?AfterNavigationHook>;

    constructor(options: RouterOptions = {}) {
        this.app = null
        this.apps = []
        this.options = options
        this.beforeHooks = []
        this.resolveHooks = []
        this.afterHooks = []
        this.matcher = createMatcher(options.routes || [], this)

        let mode = options.mode || 'hash'
        this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
        if (this.fallback) {
            mode = 'hash'
        }
        if (!inBrowser) {
            mode = 'abstract'
        }
        this.mode = mode

        switch (mode) {
            case 'history':
                this.history = new HTML5History(this, options.base)
                break
            case 'hash':
                this.history = new HashHistory(this, options.base, this.fallback)
                break
            case 'abstract':
                this.history = new AbstractHistory(this, options.base)
                break
            default:
                if (process.env.NODE_ENV !== 'production') {
                    assert(false, `invalid mode: ${mode}`)
                }
        }
    }

    match(raw: RawLocation,
          current?: Route,
          redirectedFrom?: Location): Route {
        return this.matcher.match(raw, current, redirectedFrom)
    }

    get currentRoute(): ?Route {
        return this.history && this.history.current
    }

    init(app: any /* Vue component instance */) {
        process.env.NODE_ENV !== 'production' && assert(
            install.installed,
            `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
            `before creating root instance.`
        )

        //@todo 什么时候会添加多个app？
        //猜测：一个vueRouter对象可以被设置到多个不同的vue实例中，所以该vueRouter对象的
        //init方法会被调用多次。但是app属性只会被赋值一次，如下的注释所示，有一个主app的概念
        this.apps.push(app)

        // main app already initialized.
        //@todo 主app的概念是什么意思？
        //猜测：从目前的源码中来看，源码中下需要调用Vue的$nextTick方法，而设置app属性就是为了调用
        //$nextTick方法
        if (this.app) {
            return
        }

        this.app = app

        const history = this.history

        if (history instanceof HTML5History) {
            history.transitionTo(history.getCurrentLocation())
        } else if (history instanceof HashHistory) {
            const setupHashListener = () => {
                history.setupListeners()
            }
            history.transitionTo(
                history.getCurrentLocation(),
                setupHashListener,
                setupHashListener
            )
        }

        history.listen(route => {
            this.apps.forEach((app) => {
                app._route = route
            })
        })
    }

    beforeEach(fn: Function): Function {
        return registerHook(this.beforeHooks, fn)
    }

    beforeResolve(fn: Function): Function {
        return registerHook(this.resolveHooks, fn)
    }

    afterEach(fn: Function): Function {
        return registerHook(this.afterHooks, fn)
    }

    onReady(cb: Function, errorCb?: Function) {
        this.history.onReady(cb, errorCb)
    }

    onError(errorCb: Function) {
        this.history.onError(errorCb)
    }

    push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        this.history.push(location, onComplete, onAbort)
    }

    replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        this.history.replace(location, onComplete, onAbort)
    }

    go(n: number) {
        this.history.go(n)
    }

    back() {
        this.go(-1)
    }

    forward() {
        this.go(1)
    }

    getMatchedComponents(to?: RawLocation | Route): Array<any> {
        const route: any = to
            ? to.matched
                ? to
                : this.resolve(to).route
            : this.currentRoute
        if (!route) {
            return []
        }
        return [].concat.apply([], route.matched.map(m => {
            return Object.keys(m.components).map(key => {
                return m.components[key]
            })
        }))
    }

    resolve(to: RawLocation,
            current?: Route,
            append?: boolean): {
        location: Location,
        route: Route,
        href: string,
        // for backwards compat
        normalizedTo: Location,
        resolved: Route
    } {
        const location = normalizeLocation(
            to,
            current || this.history.current,
            append,
            this
        )
        const route = this.match(location, current)
        //redirectedFrom的含义可查看declarations.js文件中的注释
        const fullPath = route.redirectedFrom || route.fullPath
        const base = this.history.base
        const href = createHref(base, fullPath, this.mode)
        return {
            location,
            route,
            href,
            // for backwards compat
            normalizedTo: location,
            resolved: route
        }
    }

    addRoutes(routes: Array<RouteConfig>) {
        this.matcher.addRoutes(routes)
        if (this.history.current !== START) {
            this.history.transitionTo(this.history.getCurrentLocation())
        }
    }
}

function registerHook(list: Array<any>, fn: Function): Function {
    list.push(fn)
    return () => {
        const i = list.indexOf(fn)
        if (i > -1) list.splice(i, 1)
    }
}

function createHref(base: string, fullPath: string, mode) {
    var path = mode === 'hash' ? '#' + fullPath : fullPath
    return base ? cleanPath(base + '/' + path) : path
}

VueRouter.install = install
VueRouter.version = '__VERSION__'

if (inBrowser && window.Vue) {
    window.Vue.use(VueRouter)
}
